/*                                  ATELIER                                   */

/* %%LICENSE_TAG%%                                                            */
package edu.gmu.view.cube;

import edu.gmu.atelier.AffineVector;
import edu.gmu.atelier.AffineVectorFactory;
import edu.gmu.atelier.Matrix;
import edu.gmu.atelier.VectorFactory;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics2D;

/**
 * The Pen uses a Transform to draw points, lines, and various other
 * geometric shapes.  Pen was somewhat modeled after the
 * OpenGL Vertex Transformation.
 * <p>
 * Pen wraps the primitive Java2D drawing tool and only uses the
 * drawLine method.  All transformations (e.g. rotations, translations,
 * scaling, perspective projection, clipping, viewport projections) are
 * performed by this class without using the Java2D utilities.
 * @see http://www.songho.ca/opengl/gl_transform.html#projection
 * @author  James H. Pope
 * @version $Revision:$ $Date:$
 */
public class Pen
{
    public static final int X = 0;
    public static final int Y = 1;
    public static final int Z = 2;
    public static final int W = 3;
    
    /**
     * Projection for modelView (transforms objectCoords --> eyeCoords)
     */
    private Transform modelView = null;

    /**
     * Projection for perspective   (transforms eyeCoords  --> clipCoords)
     * Includes perspective divions (transforms clipCoords --> ndcCoords)
     */
    private Transform perspective = null;

    /**
     * Viewport transformation (transforms ndcCoords --> windowCoords)
     */
    private Transform viewport = null;

    /**
     * Graphics primitive used to draw points and lines
     * <p>
     * Note: Only used for basic rendering, all transformations are done
     * using our own mechanisms (e.g. affine transforms) - and our
     * coordinate system's center is lower left instead of upper left
     */
    private Graphics2D graphics = null;
    
    private VectorFactory factory = new AffineVectorFactory();

//    /**
//     * Creates a new Pen with specified values.
//     * @modifies graphics.affineTransform
//     * @param graphics
//     */
//    public Pen( Graphics2D graphics )
//    {
//        this.modelView = new Transform();
//        this.perspective = new Transform();
//        this.graphics = graphics;
//    }

//    /**
//     * Creates a new Pen with specified values.
//     * @param graphics
//     * @param x viewport
//     * @param y viewport
//     * @param w viewport
//     * @param h viewport
//     */
//    public Pen( Graphics2D graphics, double x, double y, double w, double h )
//    {
//        this.modelView   = new Transform();
//        this.perspective = new Transform();
//        this.viewport    = new Transform();
//
//        this.graphics = graphics;
//
//        this.setViewport(x, y, w, h);
//    }
    
    /**
     * Creates a new Pen with specified values.
     * @param graphics
     * @param component
     */
    public Pen( Graphics2D graphics, Component component )
    {
        this.modelView   = new Transform();
        this.perspective = new Transform();
        this.viewport    = new Transform();
        this.viewport.push(); // This is so we can pop without error

        this.graphics = graphics;

        // Default viewport is whole component, can be changed
        double x = 0;
        double y = 0;
        double w = component.getWidth();
        double h = component.getHeight();
        this.setViewport(x, y, w, h);
    }

    /**
     * Gets the Projection matrix
     * @return projection
     */
    public Transform getProjection( )
    {
        return this.perspective;
    }

    /**
     * Gets the ModelView matrix
     * @return modelView
     */
    public Transform getModelView( )
    {
        return this.modelView;
    }
    
    /**
     * Processes object point through the modelView projection and
     * the perspective projection.
     * @param pObject
     * @return pPerspective
     */
    protected AffineVector transformPipeline( AffineVector pObject )
    {
        /*
         * Transform the model and view (e.g. rotations, translations, scalings)
         */
        AffineVector pEye  = this.modelView.transform(pObject);
        
        /*
         * Transform to the screen
         */
        AffineVector pClip = this.perspective.transform(pEye);

        /*
         * Divide by w - "perspective division" to put point in unit box
         * If outside this box, it should be clipped.
         */
        if( pClip.getElement(W).doubleValue() == 0.0 )
        //if( pClip.getW() == 0.0 )
        {
            /*
             * I would have thought that a division by zero resulted in
             * an ArithmeticException - but instead it returns a double
             * of type "divisionInfinity".  Thus we could let this
             * continue and would be clipped (i.e. return null).
             *
             * Just in case, we go ahead here and return null as
             * to avoid the mysterious division by zero.
             */
            //System.out.println( "pClip.getW()="+pClip.getW() + " bad division" + (1.0/0.0) );
            //return new PointCol(0.0, 0.0, 0.0);
            return null;
        }
        double w = pClip.getElement(W).doubleValue();
        double x = pClip.getElement(X).doubleValue();
        double y = pClip.getElement(Y).doubleValue();
        double z = pClip.getElement(Z).doubleValue();
        AffineVector pNdc = new AffineVector(   x / w,
                                                y / w,
                                                z / w );
        //System.out.println( "pNdc="+pNdc );
        //System.out.println( "pNdc("+(count++)+")="+pNdc );

        /*
         * Perform clipping on normalized point - actually need to change
         * this to evaluate lines that can be cut if necessary by the
         * clipping planes.
         */
        double pNdcX = pNdc.getElement(X).doubleValue();
        double pNdcY = pNdc.getElement(Y).doubleValue();
        double pNdcZ = pNdc.getElement(Z).doubleValue();
        if(     pNdcX > 1.0 || pNdcX < -1.0     ||
                pNdcY > 1.0 || pNdcY < -1.0     ||
                pNdcZ > 1.0 || pNdcZ < -1.0 )
        {
            //System.out.println("Bailing: x="+pNdcX+" y="+pNdcY + " z="+pNdcZ);
            return null;
        }

        /*
         * pNdc (normalized device coords) is a unit point (in unit box)
         * Now scale/translate into width/height of the window
         */
        AffineVector pWindow = this.viewport.transform(pNdc);
        //System.out.println( "pNdc norm="+pWindow.toString() );
        
        return pWindow;
    }

    /**
     * Draws line according to current transformation, assumes zero origin.
     * @param point2
     */
    public void drawLine( AffineVector v )
    {
        this.drawLine(new AffineVector(0.0, 0.0, 0.0), v );
    }
    
    /**
     * Draws line according to current transformation.
     * @param point1
     * @param point2
     */
    public void drawLine( double p1x, double p1y, double p2x, double p2y )
    {
        this.drawLine(new AffineVector(p1x, p1y), new AffineVector(p2x, p2y) );
    }

    /**
     * Draws line according to current transformation.
     * @param point1
     * @param point2
     */
    public void drawLine( AffineVector point1, AffineVector point2 )
    {
        // Transform does not modify passed point - makes a copy to return to us
        AffineVector p1 = this.transformPipeline(point1);
        AffineVector p2 = this.transformPipeline(point2);

        // If either point was clipped - do not draw line
        // really need to "cut" line by clipping plane
        if( p1 == null || p2 == null )
        {
            return;
        }
        
        double x1 = p1.getElement(X).doubleValue();
        double y1 = p1.getElement(Y).doubleValue();
        
        double x2 = p2.getElement(X).doubleValue();
        double y2 = p2.getElement(Y).doubleValue();

        this.graphics.drawLine( (int)x1, (int)y1,
                                (int)x2, (int)y2);
    }

    /**
     * Draws triangle according to current transformation.
     * @param point1
     * @param point2
     * @param point3
     */
    public void draw( Polygon r )
    {
        // Transform does not modify passed point - makes a copy to return to us
        // Not sure it makes a difference - draw clockwise
        AffineVector p1 = this.transformPipeline(r.lowerLeft);
        AffineVector p2 = this.transformPipeline(r.upperLeft);
        AffineVector p3 = this.transformPipeline(r.upperRight);
        AffineVector p4 = this.transformPipeline(r.lowerRight);
        
        double x1 = p1.getElement(X).doubleValue();
        double y1 = p1.getElement(Y).doubleValue();
        
        double x2 = p2.getElement(X).doubleValue();
        double y2 = p2.getElement(Y).doubleValue();
        
        double x3 = p3.getElement(X).doubleValue();
        double y3 = p3.getElement(Y).doubleValue();
        
        double x4 = p4.getElement(X).doubleValue();
        double y4 = p4.getElement(Y).doubleValue();

        this.graphics.drawLine( (int)x1, (int)y1,
                                (int)x2, (int)y2);

        this.graphics.drawLine( (int)x2, (int)y2,
                                (int)x3, (int)y3);

        this.graphics.drawLine( (int)x3, (int)y3,
                                (int)x4, (int)y4);

        this.graphics.drawLine( (int)x4, (int)y4,
                                (int)x1, (int)y1);
    }

    /**
     * Sets the color state for the pen
     * @param color
     */
    public void setColor( Color color )
    {
        // simply delegate
        this.graphics.setColor(color);
    }


    //------------------------------------------------------------------------//
    // Perspective Projection Matrix
    //------------------------------------------------------------------------//

    /**
     * Sets the Perspective Projection Matrix to top, bottom, left, right,
     * near, and far from specified parameters.
     * @param fovy (field of view y and x? in radians)
     * @param aspect(ratio of screen expressed as width/height)
     * @param znear (must be positive)
     * @param zfar  (must be positive)
     *
     * @throws IllegalArgumentException if aspect is negative
     * @throws IllegalArgumentException if znear is zero or negative
     * @throws IllegalArgumentException if zfar is negative or <= znear
     */
    public void setPerspective( double fovy,
                                double aspect,
                                double znear,
                                double zfar )
    {
        // Precondition checks
        if( aspect < 0 )
        {
            String s = "Param aspect cannot be negative: "+aspect;
            throw new IllegalArgumentException(s);
        }
        if( znear <= 0 )
        {
            String s = "Param znear cannot be zero or negative: "+znear;
            throw new IllegalArgumentException(s);
        }
        if( zfar < 0 )
        {
            String s = "Param zfar cannot be negative: "+zfar;
            throw new IllegalArgumentException(s);
        }
        if( zfar <= znear )
        {
            String s = "Param zfar (" + zfar +
                    ") cannot less than or equal to znear ("+znear+")";
            throw new IllegalArgumentException(s);
        }

        double top    = znear * Math.tan( fovy );
        double bottom = -top;
        double right  = top * aspect;
        double left   = -right;

        this.setPerspective(top, bottom, right, left, znear, zfar);
    }

    /**
     * Sets the Perspective Projection Matrix to top, bottom, left, right,
     * near, and far from specified parameters.
     * @param top
     * @param bottom
     * @param right
     * @param left
     * @param znear
     * @param zfar
     */
    public void setPerspective( double t,
                                double b,
                                double r,
                                double l,
                                double n,
                                double f )
    {
        /*
         * Note: Intuition is your "eye" is at z=0 looking "down/away" along z-axis
         * Any positive z is "behind" you (your eye) and will not be visible.
         * The projection treats them as if negative.
         * Thus near=1 and far=10 is conceptually near=-1 and far=-10
         * 
         * Perspective has to do with the camera and the "view" box/volume.
         * The near plane and far plane are actually in the -z direction.
         * This is a little confusing, object has to be in between the planes
         * and therefore -znear < object < -zfar, i.e. negative z position.
         * Example:
         * znear=1 zfar=10 translate object to z=+5 it will not be displayed!
         * znear=1 zfar=10 translate object to z=-5 it will be displayed.
         * This is because -5 is between -1 and -10 and +5 is not.
         */
        
        // Look at http://en.wikipedia.org/wiki/3D_projection
        //         http://www.songho.ca/opengl/gl_projectionmatrix.html
        //         http://gambasdoc.org/help/comp/gb.opengl/gl/frustum
        //double v = Double.NaN;
        
        double[][] perspectiveValues = new double[][]
        {
            {   2*n/(r-l),         0, (r+l)/(r-l),           0 },
            {           0, 2*n/(t-b), (t+b)/(t-b),           0 },
            {           0,         0,-(f+n)/(f-n), -2*f*n/(f-n)}, // make negative??? perspective right-handes, ndc left-handed?
            {           0,         0,          -1,           0 },
        };
        
        Matrix perspectiveMatrix = this.factory.makeMatrix(perspectiveValues);
        //Matrix perspectiveMatrix = new Matrix(perspectiveValues);
        //System.out.println( "perspectiveMatrix="+perspectiveMatrix );
        this.perspective.multiply(perspectiveMatrix);
    }

    /**
     * Sets the Viewport Projection matrix.  Note, this has nothing to
     * do with the "View"/camera transformation (see LookAt).
     * Viewport is essentially the translating and rescaling of
     * the normalized device coordinates into window coordinates.
     * @param x
     * @param y
     * @param w
     * @param h
     */
    public void setViewport( double x,
                             double y,
                             double w,
                             double h )
    {
        /*
         * Now map/scale into width/height of the screen
         */
        //PointCol pWindow = new PointCol(    (pNdc.getX() * 0.5 * w) + (x + 0.5*w),
        //                                    (pNdc.getY() * 0.5 * h) + (y + 0.5*h),
        //                                    0.0);

        /*
         * Another method depthRange is normally used to determine the
         * linear function (multiplier) for the normalized z [+1,-1] values
         */
        //this.viewport.translate(x+0.5*w, y+0.5*h, 0.0); // no z translate
        //this.viewport.scale(0.5*w, 0.5*h, 1.0);         // no z scale
        
        // Always remove previous setting, unlike others stack only one deep
        this.viewport.pop();
        this.viewport.push();
        this.viewport.translate(x+0.5*w, y+0.5*h, 0.0); // no z translate
        this.viewport.scale(0.5*w, 0.5*h, 1.0);         // no z scale
    }

    /**
     * Here we translate/scale the z-ndc linearly from [+1,-1] to the
     * near and far planes (different than the perspective near and far
     * planes).
     * <p>
     * <pre>
     *   pWindow.z = ((f-n)/2)*pNdc.z + ((f+n)/2)
     * </pre>
     * @param n
     * @param f
     */
    public void setDepthRange( double n, double f )
    {
        this.viewport.translate(0.0, 0.0, ((f+n)/2.0)); // no x,y translate
        this.viewport.scale(1.0, 1.0, ((f-n)/2.0));     // no x,y scale
    }
    //------------------------------------------------------------------------//
    
    /*
     * OpenGL does not technically allow the camera position to be changed,
     * it is always at (0,0,0) looking down the -z axis.  The way this
     * is done is by essentially transforming the world coordinates
     * to the eye/camera point of view.  This is typically done on the
     * the projection matrix instead of the modelView matrix.
     * 
     * Look at similar to OpenGL.  Essentially changes the camera position.
     * http://www.songho.ca/opengl/gl_transform.html and
     * http://www.opengl.org/sdk/docs/man2/xhtml/gluLookAt.xml
     *
     * Instead of using a "gluMode" function to switch which matrix
     * the gluLookAt function applies to, we (IMHO) locate the
     * gluLookAt code with the transform (which is essentially
     * a stack'able matrix).
     */
    
//    /**
//     * Transforms the viewing/projection matrix to provide a 
//     * different looking view/angle of the model.  Note that the
//     * eye and center are specified Cartesian Coordinates, however,
//     * the up values are vectors.
//     *
//     * @param eyex the eye's x position
//     * @param eyey the eye's y position
//     * @param eyez the eye's z position
//     * @param centerx the model's center x position
//     * @param centery the model's center y position
//     * @param centerz the model's center z position
//     * @param upX the rotation angle vector perpendicular from eye to center
//     * @param upY the rotation angle vector perpendicular from eye to center
//     * @param upZ the rotation angle vector perpendicular from eye to center
//     */
//    public void setLookAt(  double eyex, double eyey, double eyez,
//                            double centerx, double centery, double centerz,
//                            double upX, double upY, double upZ)
//    {
//        /*
//         * 1. Change center into a vector
//         */
//        // gl.Translated( -eX, -eY, -eZ );
//        centerx = centerx - eyex;
//        centery = centery - eyey;
//        centerz = centerz - eyez;
//        
//        /*
//         * 2. The angle of center on xz plane ans x axis
//         */
//        // i.e. angle to rotate so center in the nagative yz plane
//        double a = Math.atan( centerz / centerx );
//        if( centerx >= 0 )
//        {
//            a = a + Math.PI / 2.0;
//        }
//        else
//        {
//            a = a - Math.PI / 2.0;
//        }
//        
//        /*
//         * 3. The angle between the center and y axis
//         */
//        // i.e. angle to rotate so center in the negative z axis
//        double centerd = Math.sqrt( centerx*centerx + centery*centery + centerz*centerz );
//        double b = Math.acos( centery / centerd );
//        b = b - Math.PI / 2.0;
//        
//        /*
//         * 4. Up rotate around y axis (a) radians
//         */
//        double upx =  upX*Math.cos(a) + upZ*Math.sin(a);
//        double upz = -upX*Math.sin(a) + upZ*Math.cos(a);
//        upX = upx;
//        upZ = upz;
//        
//        /*
//         * 5. Up rotate around x axis (b) radians
//         */
//        double upy = upY*Math.cos(b) - upZ*Math.sin(b);
//        upz = upY*Math.sin(b) + upZ*Math.cos(b);
//        upY = upy;
//        upZ = upz;
//        
//        /*
//         * 6. The angle between up on xy plane and y axis
//         */
//        double c = Math.atan( upX / upY );
//        if( upY < 0 )
//        {
//            c = c + Math.PI;
//        }
//        
//        this.perspective.rotateZ(c);
//        //gl.glRotated( Math.toDegrees(c), 0, 0, 1 );
//        
//        // Up in yz plane
//        this.perspective.rotateX(b);
//        //gl.glRotated( Math.toDegrees(b), 1, 0, 0 );
//        
//        // Center in negative z axis
//        this.perspective.rotateY(a);
//        //gl.glRotated( Math.toDegrees(a), 0, 1, 0 );
//        
//        // Center in yz plane
//        this.perspective.translate(-eyex, -eyey, -eyez);
//        //gl.glTranslated( -eyex, -eyey, -eyez );
//        
//        // eye at origin
//    }
}

/*                                  ATELIER                                   */